/*
 * Copyright (C) 2007 Moritz Tuerk <brotkasten@users.sourceforge.net>
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms 
 * of the GNU General Public License as published by the Free Software Foundation; either 
 * version 2 of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied 
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
 * See the GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License along with 
 *  this program; if not, write to the Free Software Foundation, 
 *  Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA
 */
package mo.chat;

import java.awt.Color;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.SystemColor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.ImageIcon;
import javax.swing.JEditorPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextPane;
import javax.swing.border.EtchedBorder;
import javax.swing.border.TitledBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;


/**
 * This class implements the panel used to type and display chat messages in the
 * wizard card game
 * 
 * @author Moritz Tuerk
 */
@SuppressWarnings("serial")
public class ChatPanel extends JPanel {
    private final static String SMILE_SMILE = "smiley2.png";
    private final static String SMILE_TONGUE = "smiley3.png";
    private final static String SMILE_WEIRD = "smiley1.png";
    private final static String SMILEY_PATH = "cardwizard/resource/icons/";

    private final static Pattern SMILEY_PATTERN = Pattern
            .compile(":[)]|%[)]|:P");
    private final static String SMILEYTXT_SMILE = ":)";
    private final static String SMILEYTXT_TONGUE = ":P";
    private final static String SMILEYTXT_WEIRD = "%)";

    /**
     * Will stores the smiley file names to the already loaded {@link ImageIcon}
     * instances.
     */
    private final HashMap<String, ImageIcon> bufferedSmileys;

    /**
     * panel to show the chat history
     */
    JTextPane chatWindow;

    /**
     * saves the document listener that listens to input of chat messages
     */
    InputListener inputListener;

    /**
     * panel to type in chat messages
     */
    JEditorPane inputWindow;

    /**
     * saves the local nick name, that is prepended every input typed in this
     * chat panel
     */
    private String nick;

    /**
     * saves the color of which is used to display the local nick name
     */
    private Color nickColor;

    /**
     * saves the listeners for outgoing messages
     */
    private final ArrayList<MessageSender> sendListeners;

    /**
     * saves the chat panels split pane
     */
    private final JSplitPane split;

    /**
     * Creates a new chat panel for the given nick name and the color the nick
     * name is painted in
     * 
     * @param nick
     *            saves the nick name which is prepended every message message
     *            typed in this panel
     * @param nickColor
     *            saves the color in which the nick name is painted
     */
    public ChatPanel(final String nick, final Color nickColor) {
        this.nick = nick;
        this.nickColor = nickColor;
        this.sendListeners = new ArrayList<MessageSender>();
        this.bufferedSmileys = new HashMap<String, ImageIcon>();

        setLayout(new GridBagLayout());

        final DefaultStyledDocument chatDoc = new DefaultStyledDocument();
        final DefaultStyledDocument inDoc = new DefaultStyledDocument();

        this.inputListener = new InputListener();
        this.inputListener.setChatPanel(this);

        inDoc.addDocumentListener(this.inputListener);

        this.chatWindow = new JTextPane(chatDoc);
        this.chatWindow.setEditable(false);

        this.inputWindow = new JEditorPane();
        this.inputWindow.setDocument(inDoc);

        final JScrollPane chatScroll = new JScrollPane(this.chatWindow);
        chatScroll.setAutoscrolls(true);
        chatScroll.setBorder(new TitledBorder(new EtchedBorder(
          EtchedBorder.LOWERED), "Nachrichten"));

        final JScrollPane inputScroll = new JScrollPane(this.inputWindow);
        inputScroll.setBorder(new TitledBorder(new EtchedBorder(
          EtchedBorder.LOWERED), "Eingabe"));
        this.split = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
        this.split.setTopComponent(chatScroll);
        this.split.setBottomComponent(inputScroll);

        add(this.split, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(
                        0, 0, 0, 0), 0, 0));
    }

    /**
     * is used internally to add text to the chat history window. It is called
     * to filter smileys and other text parts.
     * 
     * @param text
     *            saves the text to be appended to the chat history
     * @param doc
     *            saves the document containing the chat history
     * @param attrs
     *            the attribute set to use to append the text
     * @throws BadLocationException
     *             in practice this should not happen
     */
    private void addFilteredInput(final String text,
            final DefaultStyledDocument doc, final SimpleAttributeSet attrs)
            throws BadLocationException {

        final Matcher match = SMILEY_PATTERN.matcher(text);
        int start = 0;
        while (match.find()) {
            doc.insertString(doc.getLength(), text.substring(start, match
                    .start()), attrs);
            final String smile = match.group();

            addImageToDocument(doc, getSmiley(smile), smile);

            start = match.end();
        }
        doc.insertString(doc.getLength(), text.substring(start, text.length()),
                attrs);
    }

    private void addImageToDocument(final DefaultStyledDocument doc,
            final ImageIcon icon, final String smile)
            throws BadLocationException {
        if (icon != null) {
            final Style smiley = doc.addStyle(smile, null);
            StyleConstants.setIcon(smiley, icon);
            doc.insertString(doc.getLength(), smile, smiley);
        }
    }

    /**
     * This can be called to add input off another nick (extern input)
     * 
     * @param color
     *            saves the color in which the nick name is displayed
     * @param nick
     *            saves the nick name of the input sender
     * @param text
     *            saves the text send by the given nick
     * @throws BadLocationException
     *             in practice should not happen!
     */
    public void addInput(final Color color, final String nick, final String text)
            throws BadLocationException {
        final DefaultStyledDocument doc = (DefaultStyledDocument) this.chatWindow
                .getDocument();

        SimpleAttributeSet attrs = new SimpleAttributeSet();
        StyleConstants.setForeground(attrs, color);
        StyleConstants.setBold(attrs, Boolean.TRUE);

        final int len = doc.getLength();
        doc.insertString(len, nick + ": ", attrs);

        String tmp = text == null ? "" : text;
        if (!tmp.endsWith("\n")) {
            tmp += "\n";
        }

        attrs = new SimpleAttributeSet();
        addFilteredInput(tmp, doc, attrs);

        this.chatWindow.scrollRectToVisible(this.chatWindow.modelToView(doc
                .getLength() - 1));

    }

    /**
     * adds a new instance for sending messages to chat participients
     * 
     * @param newSender
     *            the message sending instance
     */
    public void addMessageSender(final MessageSender newSender) {
        this.sendListeners.add(newSender);
    }

    /**
     * Removes all registered {@link MessageSender} objects.<br>
     */
    public void clearMessageSender() {
        this.sendListeners.clear();
    }

    /**
     * is called from the input listener to append local input
     * 
     * @param text
     *            saves the text to be appended to the chat history
     * @throws BadLocationException
     *             in practice this should not happen!
     */
    void fireInput(final String text) throws BadLocationException {
        final DefaultStyledDocument doc = (DefaultStyledDocument) this.inputWindow
                .getDocument();
        doc.remove(0, doc.getLength());

        final Iterator<MessageSender> sendIter = this.sendListeners.iterator();
        while (sendIter.hasNext()) {
            sendIter.next().sendMessage(this.nick, text);
        }

        addInput(this.nickColor, this.nick, text);
    }

    /**
     * call this to get the split pane of the chat panel (mainly to set the
     * divider location)
     * 
     * @return the split pane that splits the input panel from the chat history
     *         panel
     */
    public JSplitPane getChatSplit() {
        return this.split;
    }

    private ImageIcon getSmiley(final String smile) {
        ImageIcon result = this.bufferedSmileys.get(smile);
        if (result == null) {
            final ClassLoader cl = Thread.currentThread()
                    .getContextClassLoader();
            if (smile.equals(SMILEYTXT_SMILE)) {
                result = new ImageIcon(cl
                        .getResource(SMILEY_PATH + SMILE_SMILE));
            } else if (smile.equals(SMILEYTXT_WEIRD)) {
                result = new ImageIcon(cl
                        .getResource(SMILEY_PATH + SMILE_WEIRD));
            } else if (smile.equals(SMILEYTXT_TONGUE)) {
                result = new ImageIcon(cl.getResource(SMILEY_PATH
                        + SMILE_TONGUE));
            }
            this.bufferedSmileys.put(smile, result);
        }

        return result;
    }

    /**
     * removes the specified message sender.<br>
     * 
     * @param toRemove
     *            message sender to remove.
     */
    public void removeMessageSender(final MessageSender toRemove) {
        this.sendListeners.remove(toRemove);
    }

    /**
     * @param nickColor
     */
    public void setColor(final Color nickColor) {
        this.nickColor = nickColor;
    }

    /**
     * used to enable or disable the chat panel (enables/disables input)
     */
    @Override
    public void setEnabled(final boolean enable) {
        this.chatWindow.setEnabled(enable);
        this.inputWindow.setEnabled(enable);

        Color c = SystemColor.control;
        if (enable) {
            c = Color.white;
        }
        this.chatWindow.setBackground(c);
        this.inputWindow.setBackground(c);
    }

    public void setNick(final String nick) {
        this.nick = nick;
    }
}
